<!-- eslint-disable no-undef -->
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import DialogTips from './component/DialogTips.vue'
import ActionMenu from './component/ActionMenu.vue'
import LoadGame from './component/LoadGame.vue'
import { getConfigList } from '@/api/user'
import mitt from '@/mitt'
import { useWindowSize, useThrottleFn } from '@vueuse/core'
import { getQueryParams } from '@/utils/tools'
import { storeToRefs } from 'pinia'
import useMenuStore from '@/pinia/menuStore'
import useUserStore from '@/pinia/userInfo'
import { useMouse } from '@/hooks/mouse.js'
import { useBroad } from '@/hooks/broad.js'
import { initProtobuf } from '@/utils/initProto'
const isConnected = ref(false)
const menuStore = useMenuStore()
const userStore = useUserStore()
const { isMouseLock, screenType, isStream, micPercent } = storeToRefs(menuStore)
const { userInfo, token } = storeToRefs(userStore)
const videoStream = ref(null)
const videoBox = ref(null)
const cursorRef = ref(null)
const { offsetX, offsetY, addMouseEvent, removeMouseEvent } = useMouse()
const { addBroadEvent, removeBroadEvent } = useBroad()
const { width, height } = useWindowSize()
const statsDetail = ref({ frameWidth: 0 })
const dialogVisible = ref(false)
const dialogTitle = ref('')
const isTimeout = ref(false)
let loadContentParam = {}
let socket = null
let beatTimer = null
let statsTask = null
let remoteId = null
let gameId = null
let isCloseGame = false
let dispatchId = null
let rtcConfiguration = null
let session = ''
let MyMessage, MsgType, ReportType

/**
 * @description 配置发送socket消息的格式
 */
const getParams = (msgType, param) => {
  return {
    msgType,
    base: {
      timestamp: new Date().getTime() + '',
      endpointType: 1,
      endpointId: userInfo.value.uid + '',
      dispatchId
    },
    ...param
  }
}

/**
 * @description 初始化Protobuf配置文件
 */
const initProtoConfig = async () => {
  const protoInfo = await initProtobuf()
  MyMessage = protoInfo.MyMessage
  MsgType = protoInfo.MsgType
  ReportType = MyMessage.parent.ReportParam.ReportType
}

/**
 * @description 初始化连接
 */
onMounted(async () => {
  const search = getQueryParams()
  gameId = search.gameId
  dispatchId = search.dispatchId
  // electron跳转时通过token获取用户信息
  if (search.token) {
    token.value = decodeURI(search.token)
    localStorage.setItem('game-token', token.value)
    const { data } = await getConfigList({
      type: 1
    })
    userInfo.value = data
  }
  document.addEventListener('pointerlockchange', lockChange, false)

  initProtoConfig()

  if (token.value) {
    rtcConfiguration = {
      iceServers: [
        {
          urls: userInfo.value.ice.stun.url
        },
        {
          urls: [userInfo.value.ice.turn.url],
          credential: userInfo.value.ice.turn.pwd,
          username: userInfo.value.ice.turn.user
        }
      ]
      // iceTransportPolicy: 'relay' // 转为中继模式
    }
    setTimeout(() => {
      initP2p()
      createP2PConnection()
    }, 600)
  } else {
    closeWebsocket('程序启动失败，请关闭浏览器后重试')
  }

  // 关闭游戏
  mitt.on('closeGame', (tag = false) => {
    sendCloseGame(tag)
  })

  // 监听关闭浏览器
  window.onbeforeunload = (event) => {
    if (!isCloseGame) {
      event.preventDefault()
    }
    window.localStorage.removeItem('isPlayGame')
  }

  // 监听message和storage
  window.addEventListener('message', (event) => {
    if (event.data === 'closeGame') {
      sendCloseGame(false)
    }
  })

  // 监听storage数据变化
  window.addEventListener('storage', (event) => {
    if (event.storageArea.closeGameWindow) {
      window.localStorage.removeItem('closeGameWindow')
      sendCloseGame(false)
    }
  })
  window.localStorage.setItem('isPlayGame', 'yes')
})

/**
 * @description 卸载定时器和事件监听
 */
onUnmounted(() => {
  statsTask && clearInterval(statsTask)
  beatTimer && clearInterval(beatTimer)
  document.removeEventListener('pointerlockchange', lockChange, false)
})

/**
 * @description 监听鼠标的移动设置自定义指针的位置
 */
watch([offsetX, offsetY], () => {
  if (isMouseLock.value) {
    offsetX.value = Math.max(0, Math.min(videoStream.value.offsetWidth, offsetX.value))
    offsetY.value = Math.max(0, Math.min(videoStream.value.offsetHeight, offsetY.value))
    cursorRef.value.style.transform = 'translate(' + offsetX.value + 'px,' + offsetY.value + 'px)'
  }
})

/**
 * @description 监听收到视频流的尺寸同时设置video标签的尺寸
 */
watch(
  () => statsDetail.value.frameWidth,
  (n, o) => {
    if (!o && n > 0) {
      setVideoSize()
    }
  }
)

/**
 * @description 设置音量
 */
watch(
  () => micPercent.value,
  () => {
    videoStream.value.volume = micPercent.value / 100
  }
)

/**
 * @description 监听屏幕尺寸变化
 */
watch([width, height], () => {
  throttleSize()
})
const throttleSize = useThrottleFn(() => {
  setVideoSize()
}, 100)

/**
 * @description 添加键盘鼠标监听事件
 */
watch(
  () => isStream.value,
  (val) => {
    if (val) {
      // 添加监听事件
      console.log('添加键鼠事件...')
      addBroadEvent(p2p, remoteId)
      if (!isMouseLock.value) {
        addMouseEvent(videoStream.value, p2p, remoteId)
      }
    } else {
      // 添加监听事件
      console.log('移除键鼠事件...')
      removeMouseEvent()
      removeBroadEvent()
    }
  }
)

/**
 * @description 编码消息
 */
const encodeMsg = (message) => {
  const wsMessage = MyMessage.create(message)
  const buffer = MyMessage.encode(wsMessage).finish()
  return buffer
}

/**
 * @description 解码消息
 */
const decodeMsg = async (blob) => {
  const buffer = await blob.arrayBuffer()
  const data = new Uint8Array(buffer)
  const message = MyMessage.decode(data)
  return new Promise((resolve) => {
    resolve(message)
  })
}

/**
 * @description webesocket断开重连, 每个20s重连一次，连接3次
 */
let connectTotal = 0
let connectTimer = null
const reconnectionSocket = (tag = false) => {
  console.log('websocket reconnection..')
  if (connectTotal > 2) {
    connectTotal = 0
    connectTimer && clearInterval(connectTimer)
    console.log('websocket reconnection error')
    return
  }
  connectTotal++
  createP2PConnection(tag)
}

/**
 * @description websocket封装模块，主要用于终端主机的交互
 */
const closeWebsocket = (title = '') => {
  isCloseGame = true
  setTimeout(() => {
    dialogVisible.value = true
    if (!isStream.value) {
      dialogTitle.value = title || '程序启动失败，请关闭页面重新尝试'
    } else {
      dialogTitle.value = '程序已断开，请关闭页面重新尝试'
    }
  }, 2000)
  socket && socket.close()
}
class SignalingChannel {
  constructor() {}
  connect(params) {
    const _self = this
    socket = new WebSocket(params.host)
    socket.onopen = () => {
      console.log('WebSocket连接成功...')
      isConnected.value = true
      if (connectTimer) clearInterval(connectTimer)
      if (beatTimer) clearInterval(beatTimer)
      beatTimer = setInterval(() => {
        sendheartbeat()
      }, 1000 * 30)
      socketLogin()
      if (params.tag && isStream.value) return
      startGame()
    }
    // 监听消息
    socket.onmessage = async (event) => {
      const blob = event.data
      const message = await decodeMsg(blob)
      console.log('收到消息')
      console.log(message)
      if (message.msgType === MsgType.LEAVE_NOTIFY) {
        closeWebsocket('用户已退出登录')
      } else if (message.msgType === MsgType.LOGIN_NOTIFY) {
        // 禁止用户继续启动游戏
        if (message.loginNotifyParam.result == 1) {
          closeWebsocket('您已达到连接限制')
          return
        }
        // 收到登录回复消息-包括session
        session = message.base.session
        window.sessionStorage.setItem('loginSession', session)
        const myId = userInfo.value.uid + '-' + session
        p2p.setMyId(myId)
      } else if (message.msgType === MsgType.LOAD_CONTENT_NOTIFY) {
        // 收到LOAD_CONTENT消息
        loadContentParam = message.loadContentNotifyParam.drwp
        window.sessionStorage.setItem('LOAD_CONTENT_NOTIFY', JSON.stringify(loadContentParam))
        // 如果flag状态为6，则调度失败, 为8时为调度关闭
        if (
          message.loadContentNotifyParam.flag !== 7 &&
          message.loadContentNotifyParam.flag != 4 &&
          message.loadContentNotifyParam.flag != 5 &&
          message.loadContentNotifyParam.flag != 1
        ) {
          if (message.loadContentNotifyParam.flag === 6) {
            if (loadContentParam.failedReason.indexOf('no provider available') > -1) {
              closeWebsocket('资源不足，请联系工作人员或者稍后重试')
            } else if (message.loadContentNotifyParam.reason == 1) {
              closeWebsocket('系统错误，请联系工作人员或者稍后重试')
            } else if (message.loadContentNotifyParam.reason == 2) {
              closeWebsocket('系统错误，请联系工作人员')
            }
          } else {
            closeWebsocket()
          }
          return
        }
      } else if (message.msgType === MsgType.START_P2P_CONNECT_NOTIFY) {
        // 收到匹配消息 可设置远端id
        window.sessionStorage.setItem(
          'START_P2P_CONNECT_NOTIFY',
          JSON.stringify(message.startP2pConnectNotifyParam)
        )
        remoteId = message.startP2pConnectNotifyParam.remoteId
        setRemoteIds(remoteId)
      } else if (message.msgType === MsgType.EXCHANGE_P2P_NOTIFY) {
        // 收到媒体协商交换消息
        const msg = message.exchangeP2pParam.msg
        // 处理异常场景-需要重新发起调度
        if (msg.indexOf('PwtEvent') > -1 && msg.indexOf('reconnect' > -1)) {
          startGame()
          return
        }
        _self.onMessage(remoteId, msg)
      }
    }

    socket.onclose = () => {
      isConnected.value = false
      beatTimer && clearInterval(beatTimer)
      connectTimer && clearInterval(connectTimer)
      console.log('WebSocket连接已关闭...')
      try {
        p2p && p2p.closeSignal()
      } catch (err) {
        console.log(err)
      }
      // 手动关闭不用重新连接
      if (isCloseGame) {
        return
      }
      setTimeout(() => {
        reconnectionSocket(true)
      }, 2000)
      connectTimer = setInterval(() => {
        reconnectionSocket(true)
      }, 10000)
    }
    return new Promise((resolve) => {
      resolve(params.token)
    })
  }
  send(targetId, data) {
    const message = getParams(MsgType.EXCHANGE_P2P, {
      exchangeP2pParam: {
        from: userInfo.value.uid + '-' + session,
        to: targetId,
        msg: data
      }
    })
    sendMessage(message)
    return new Promise((resolve) => {
      resolve()
    })
  }
  disconnect() {
    socket && socket.close()
  }
}

/**
 * @description 发送socket用户登录信息
 */
const socketLogin = () => {
  const message = getParams(MsgType.LOGIN, {
    loginParam: {
      token: token.value,
      session: window.sessionStorage.getItem('loginSession') || ''
    }
  })
  sendMessage(message)
}

/**
 * @description 启动游戏
 */
const startGame = () => {
  const message = getParams(MsgType.LOAD_CONTENT, {
    loadContentParam: {
      contentId: gameId
    }
  })
  sendMessage(message)
}

/**
 * @description 上报p2p连接成功状态
 */
const reportP2pStatus = () => {
  const message = getParams(MsgType.REPORT_DATA, {
    report: {
      reportType: ReportType.P2P_CONNECT_STATUS,
      status: 1
    }
  })
  sendMessage(message)
}

/**
 * @description 上报收到流
 */
const reportStreamStatus = () => {
  const message = getParams(MsgType.REPORT_DATA, {
    report: {
      reportType: ReportType.GET_STREAM_STATUS,
      status: 1
    }
  })
  sendMessage(message)
}

/**
 * @description 发送心跳检测
 */
const sendheartbeat = () => {
  const message = getParams(MsgType.HEARTBEAT, {})
  sendMessage(message)
}

/**
 * @description 上报Leave消息
 */
const sendLeave = (params = {}) => {
  const message = getParams(MsgType.LEAVE, params)
  sendMessage(message)
}

/**
 * @description 上报关闭游戏
 */
const sendCloseGame = (tag) => {
  const message = getParams(MsgType.REPORT_DATA, {
    report: {
      reportType: tag ? ReportType.P2P_CONNECT_STATUS : ReportType.FRAME_WINDOW_CLOSED,
      status: tag ? 0 : 1
    }
  })
  sendMessage(message)
  sendLeave()
  isCloseGame = true
  try {
    disconnectP2p()
  } catch (e) {
    console.log(e)
  }
  setTimeout(() => {
    window.localStorage.removeItem('isPlayGame')
    window.close()
  }, 100)
}

/**
 * @description 发送消息
 */
const sendMessage = (message = '') => {
  if (!message) return
  console.log('发送消息')
  console.log(message)
  const buffer = encodeMsg(message)
  socket.send(buffer)
}

/**
 * @description 初始化p2p连接
 */
let p2p = null
const initP2p = () => {
  const signaling = new SignalingChannel()
  p2p = new Owt.P2P.P2PClient(
    {
      audioEncodings: true,
      videoEncodings: [
        {
          codec: {
            name: 'h264'
          }
        },
        {
          codec: {
            name: 'vp9'
          }
        },
        {
          codec: {
            name: 'vp8'
          }
        }
      ],
      rtcConfiguration
    },
    signaling
  )
  /**
   * p2p监听流
   */
  p2p.addEventListener('streamadded', (e) => {
    console.log('p2p streamadded')
    // 上报数据
    reportP2pStatus()
    reportStreamStatus()
    // 监听流是否断开
    e.stream.addEventListener('ended', () => {
      clearInterval(statsTask)
      console.log('Stream is removed...')
    })
    // 判断是否收到流
    if (e.stream.source.audio || e.stream.source.video) {
      videoStream.value.srcObject = e.stream.mediaStream
    }
    // 监听推流统计信息
    statsTask = setInterval(() => {
      getStreamStats()
    }, 1000)
  })

  /**
   * 监听鼠标信息
   */
  p2p.addEventListener('messagereceived', () => {})
}

/**
 * @description 创建webscoket链接
 * @param { Boolean } type false: 为初次连接 true： 为重连
 */
const createP2PConnection = async (tag = false) => {
  try {
    const protocol = window.location.protocol
    const wsProtocol = protocol.indexOf('https') > -1 ? 'wss' : 'ws'
    const host = userInfo.value.signal_url.find((item) => item.split(':')[0] === wsProtocol)
    await p2p.connect({
      host: host,
      token: userInfo.value.uid,
      tag
    })
  } catch (err) {
    console.log(err)
  }
}

/**
 * 设置远端id
 */
const setRemoteIds = (remoteId) => {
  p2p.allowedRemoteIds = [remoteId]
}

/**
 * @description 获取推流信息
 */
const getStreamStats = async () => {
  const statsResult = await p2p.getStats(remoteId)
  statsResult.forEach((stats) => {
    if (stats.type === 'inbound-rtp' && stats.kind === 'video') {
      statsDetail.value = stats
      getPacketsReceived(stats.packetsReceived)
    }
  })
}

/**
 * @description 判断收流和断流
 */
let lose_stream_count = 0
let isGetStream = false
let last_packets_received = 0
const getPacketsReceived = (packetsReceived) => {
  // 如果packetsReceived数据比上一次大，说明已经收到流
  if (packetsReceived > last_packets_received) {
    dialogVisible.value = false
    isTimeout.value = false
    lose_stream_count = 0
    if (!isGetStream) {
      isGetStream = true
      isStream.value = true
      console.log('receive video stream')
      window.localStorage.setItem('isPlayGame', 'yes')
    }
  } else if (packetsReceived > 0 && packetsReceived === last_packets_received) {
    lose_stream_count++
    if (lose_stream_count > 6) {
      // 断流时提示刷新浏览器
      dialogVisible.value = true
      dialogTitle.value = '网络异常，请检测网络或刷新页面重新连接'
      isTimeout.value = true
      console.log('disconnect video stream')
    }
  }
  last_packets_received = packetsReceived
}

/**
 * @description 断开p2p连接
 */
const disconnectP2p = () => {
  try {
    p2p.stop(remoteId)
    p2p.clearEventListener('streamadded')
    p2p.clearEventListener('messagereceived')
    p2p.disconnect()
  } catch (err) {
    console.log(err)
  }
}

/**
 * @description 设置菜单项的属性
 */
const changeAction = (obj) => {
  if (obj.type === 'size') {
    setVideoSize()
  }
}

/**
 * @description 设置视频自适应尺寸, 如果是全屏状态则不处理
 */
const setVideoSize = () => {
  if (screenType.value === 'auto') {
    if (statsDetail.value.frameWidth > statsDetail.value.frameHeight) {
      // 横屏-自适应比例
      if (statsDetail.value.frameWidth > width.value) {
        let _width = width.value
        let _height = (width.value / statsDetail.value.frameWidth) * statsDetail.value.frameHeight
        if (_height > height.value) {
          _height = height.value
          _width = (height.value / statsDetail.value.frameHeight) * statsDetail.value.frameWidth
        }
        videoBox.value.style.height = _height + 'px'
        videoBox.value.style.width = _width + 'px'
      } else {
        videoBox.value.style.width = width.value + 'px'
        videoBox.value.style.height = height.value + 'px'
      }
    } else {
      // 竖屏-自适应比例
      if (statsDetail.value.frameHeight > height.value) {
        videoBox.value.style.width =
          (height.value / statsDetail.value.frameHeight) * statsDetail.value.frameWidth + 'px'
        videoBox.value.style.height = height.value + 'px'
      } else {
        videoBox.value.style.width = width.value + 'px'
        videoBox.value.style.height = height.value + 'px'
      }
    }
  } else {
    videoBox.value.style.width = '100vw'
    videoBox.value.style.height = '100vh'
  }
}

/**
 * @description 监听鼠标锁定状态
 */
const lockChange = () => {
  if (
    document.pointerLockElement === videoStream.value ||
    document.mozPointerLockElement === videoStream.value ||
    document.webkitPointerLockElement === videoStream.value
  ) {
    addMouseEvent(videoStream.value, p2p, remoteId)
  } else {
    removeMouseEvent()
  }
}

/**
 * @description 锁定鼠标，提示用户按esc解锁鼠标
 */
const lockMouse = () => {
  videoStream.value.requestPointerLock({
    // unadjustedMovement: true
  })
}

/**
 * @description 解锁鼠标
 */
const unlockMouse = () => {
  if (
    document.pointerLockElement === videoStream.value ||
    document.mozPointerLockElement === videoStream.value ||
    document.webkitPointerLockElement === videoStream.value
  ) {
    document.exitPointerLock()
  } else {
    // 切换默认解锁模式时添加鼠标事件
    addMouseEvent(videoStream.value, p2p, remoteId)
  }
}

/**
 * @description 点击视频画面锁定鼠标
 */
const clickLockMouse = (event) => {
  // 开启声音
  if (videoStream.value.muted) {
    videoStream.value.muted = false
  }
  // 判断鼠标是否需要锁屏以及是否处在锁屏状态
  if (isMouseLock.value && !document.pointerLockElement) {
    if (!offsetX.value)
      cursorRef.value.style.transform = 'translate(' + event.offsetX + 'px,' + event.offsetY + 'px)'
    lockMouse()
  }
}
</script>
<template>
  <div class="peer-stream">
    <!--调度失败...-->
    <DialogTips
      type="error"
      :timeout="isTimeout"
      :title="dialogTitle"
      v-model="dialogVisible"
      v-if="dialogVisible"
    ></DialogTips>
    <!--游戏加载中...-->
    <LoadGame v-model="isStream"></LoadGame>
    <!--配置菜单信息-->
    <action-menu
      :stats-detail="statsDetail"
      @changeAction="changeAction"
      @lockMouse="lockMouse"
      @unlockMouse="unlockMouse"
    ></action-menu>
    <!--游戏画面渲染-->
    <div class="video-box" ref="videoBox" v-show="isStream">
      <video
        @click="clickLockMouse"
        ref="videoStream"
        class="game-video"
        id="game-video"
        muted
        autoplay
      ></video>
      <!--鼠标锁定时显示自定义指针-->
      <div
        v-show="isMouseLock"
        ref="cursorRef"
        class="cursor-bg w-[32px] h-[32px] absolute z-10 top-0 left-0"
      ></div>
    </div>
  </div>
</template>
<style scoped lang="scss">
.peer-stream {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: #222 url('@/assets/images/big-img-bf.jpg') no-repeat;
  opacity: 0.8;
  background-size: cover;
  .video-box {
    position: fixed;
    overflow: hidden;
    z-index: 9;
    width: 0;
    height: 0;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    .cursor-bg {
      //  background: url('')
      // /  0% 0% / 100% 100%;
      pointer-events: none;
    }
    .game-video {
      width: 100%;
      height: 100%;
      object-fit: fill;
    }
  }
}
</style>
